package vampirism.core.shared;

import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;

import vampirism.core.client.MainGame;
import vampirism.core.server.ServerSide;

import java.util.List;
import java.util.Random;

//Updates all positions / collisions / functions
//Server side and Client side uses this class for all updates
public class Updater implements Runnable{
	
	public boolean isClient = true;
	public boolean gameLaunched = false;
	
	public ServerSide serverSide;
	public MainGame mainGame;
	
	public Tile[][] map;
	
	public List<Player> players;
	
	public List<Bullet> bullets;
	
	public List<Biome> biomes;
	
	private Tile tempTile;
	private Tile locationTile;
	private Player tempPlayer;
	private Body tempBody;
	private Bullet tempBullet;
	private Rectangle tempRect = new Rectangle(0,0,8,8);
	private Index tempIndex;
	private Biome tempBiome;
	
	private Tile sourceTile;
	private Tile targetTile;
	
	private Body sourceBody;
	private Body targetBody;
	
	private int prevI, prevJ, nextI, nextJ;
	
	private float nextX, nextY;
	
	private Long timeSinceLastUpdate = new Long(0);
	private float multi = 0;
	
	private Random rand = new Random(Network.seed);
	
	public Updater(Tile[][] map, List<Player> players,  List<Bullet> bullets){
		this.map = map;
		this.players = players;
		this.bullets = bullets;
	}
	
	public Updater(Tile[][] map, List<Player> players,  List<Bullet> bullets, List<Biome> biomes) {
		this.map = map;
		this.players = players;
		this.bullets = bullets;
		this.biomes = biomes;
	}

	@Override
	public void run() {
		while(true){
			update();
		}
	}
	
	public void update(){
		
		updateBodies(players);
		updateBullets();
		
	}

	//Move all bodies by specific distance depending on last update time
	//This way lag has no effect on distance/movement
	//Also check collisions
	public void updateBodies(List<? extends Body> bodies){
		
		for(int i = 0; i < bodies.size(); i++){
			
			tempBody = bodies.get(i);
			if(tempBody==null)continue;
			
			timeSinceLastUpdate = System.currentTimeMillis() - tempBody.lastMoveTime;
			if(timeSinceLastUpdate < 10)continue;
			tempBody.lastMoveTime = System.currentTimeMillis();
			multi = timeSinceLastUpdate/1000.0f; //Transferring to seconds
				
			nextX = tempBody.pos.x;
			nextY = tempBody.pos.y;
			//Calculate next position depending on agility (statsTotal[3])
			if(tempBody.moving[0]==true){nextX = tempBody.pos.x + ((10+tempBody.statsTotal[3])*multi); tempBody.lastDir = 0;}
			if(tempBody.moving[1]==true){nextX = tempBody.pos.x - ((10+tempBody.statsTotal[3])*multi); tempBody.lastDir = 1;}
			if(tempBody.moving[2]==true){nextY = tempBody.pos.y - ((10+tempBody.statsTotal[3])*multi); tempBody.lastDir = 2;}
			if(tempBody.moving[3]==true){nextY = tempBody.pos.y + ((10+tempBody.statsTotal[3])*multi); tempBody.lastDir = 3;}
			
			if(getLocationTile(nextX,nextY)==null)continue;//Can't go outside map bounds

			if(!collidesWithTile(nextX,nextY)){
				//Next position has no collisions! apply.
				tempBody.pos.x = nextX;
				tempBody.pos.y = nextY;
				changeBodyTile(tempBody);
			}else{
				if(!collidesWithTile(tempBody.pos.x,nextY)){
					//Next position collides only in the X axis
					tempBody.moving[0] = false;
					tempBody.moving[1] = false;
					tempBody.pos.y = nextY;
					changeBodyTile(tempBody);
				}else
				if(!collidesWithTile(nextX,tempBody.pos.y)){
					//Next position collides only in the Y axis
					tempBody.moving[2] = false;
					tempBody.moving[3] = false;
					tempBody.pos.x = nextX;
					changeBodyTile(tempBody);
				}else{
					//Can't move :(
					tempBody.moving = new boolean[4];
				}
			}
		}
	}
	
	//Move all bullets, check collisions
	public void updateBullets(){

		for(int i = bullets.size()-1; i >= 0; i--){
			
			tempBullet = bullets.get(i);
			if(tempBullet==null)continue;
			
			timeSinceLastUpdate = System.currentTimeMillis() - tempBullet.lastMoveTime;
			if(timeSinceLastUpdate < 10)continue;
			tempBullet.lastMoveTime = System.currentTimeMillis();
			multi = timeSinceLastUpdate/1000.0f; //Transferring to seconds
			
			nextY = tempBullet.pos.y + (float)Math.cos(tempBullet.rotation+Math.PI/2) * 70 * multi;
			nextX = tempBullet.pos.x + (float)Math.sin(tempBullet.rotation+Math.PI/2) * 70 * multi;
		
			if(!collidesWithTile(nextX, nextY)){
				//apply position, change tile
				tempBullet.pos.x = nextX;
				tempBullet.pos.y = nextY;
				changeBulletTile(tempBullet);
			}else{
				//destroy bullet
				removeBullet(tempBullet,i);
				continue;
			}
			
			//Get the body that this bullet collided with (if any)
			targetBody = collidesWithBody(tempBullet);
			
			//Get the source body of this bullet
			sourceBody = players.get(tempBullet.sourceId);
			
			//Check if sourceBody can hit the targetBody (server side only)
			if(sourceBody!=null && targetBody!=null && targetBody.index!=tempBullet.sourceId){
			
				if(!isClient){
					sourceTile = getLocationTile(sourceBody.pos.x, sourceBody.pos.y);
					if(sourceTile.biomeNum==14)continue;//players located in starting area can't deal damage
					
					targetTile = getLocationTile(targetBody.pos.x, targetBody.pos.y);
					if(targetTile.biomeNum==14)continue;//no damage dealt to players located in starting area
					
					if(!sourceBody.vampire && !targetBody.vampire)continue;//Players can't hit other players
					
					if(sourceBody.vampire && targetBody.vampire)continue;//Vampires can't hit other vampires
					
					//Call hit() on all connected clients
					serverSide.simulateHit(sourceBody.index, targetBody.index,tempBullet.damage);
					hit(sourceBody.index, targetBody.index,tempBullet.damage);//Call the same function on serverSide
				}
				
				//Remove bullet on collision - on client and server side
				//Although damage doesn't get instantly dealt on client side
				removeBullet(tempBullet,i);
				continue;
			}
			
			//Remove old bullets
			if(System.currentTimeMillis() - tempBullet.timeCreated > tempBullet.lifeTime){
				removeBullet(tempBullet,i);
			}
		}		
	}
	
	public void hit(short sourceId,short targetId,short damage){
		
		Body target = players.get(targetId);
		
		target.health -= damage;
		
		if(target.health < 0){
			if(target.vampire){
				//Target has already been killed once! - change state to dead
				target.dead = true;
				
			}else{
				//Target has never been killed - change state to vampire
				target.vampire = true;
				target.health = target.statsTotal[6];//Refill health
			}
			
			gainExp(sourceId, (short)3);
			checkGameOver();
		}
	}
	
	public void checkGameOver(){
		int vampireCount = 0;
		int playerCount = 0;
		for(int i = 0; i < players.size(); i++){
			if(players.get(i).dead)continue;
			
			if(players.get(i).vampire)
				vampireCount++;
			else
				playerCount++;
		}
		
		//Show Victory of Defeat
		if(gameLaunched)
		if(vampireCount == 0 || playerCount == 0){
			
			gameLaunched = false;
			Network.seed++;
			
			if(isClient){
				mainGame.players.clear();
				mainGame.biomes.clear();
				mainGame.bullets.clear();
				mainGame.myPlayer = null;
				
				mainGame.map = Tile.createMap();
				mainGame.biomes = Creator.createBiomes(mainGame.map);
				mainGame.clientTextureParser.getBiomeTextures();
				
				map = mainGame.map;
				biomes = mainGame.biomes;
				
				mainGame.clientSide.haveStartedGame = false;
				
				if(vampireCount == 0)mainGame.changeMenu(4);
				else mainGame.changeMenu(5);
			}else{
				serverSide.players.clear();
				serverSide.biomes.clear();
				serverSide.bullets.clear();
				
				serverSide.map = Tile.createMap();
				serverSide.biomes = Creator.createBiomes(serverSide.map);
				
				map = serverSide.map;
				biomes = serverSide.biomes;
				
				//Resetting stats of all players on server side
				for(int i = 0; i < serverSide.accounts.size(); i++){
					serverSide.accounts.get(i).alive = null;
					for(int j = 0; j < serverSide.accounts.get(i).players.size();j++){
						tempPlayer = serverSide.accounts.get(i).players.get(j);
						tempPlayer.initStats();
					}
				}
			}
		}
	}

	//Check if rectangle at given position collides with any solid tiles
	public boolean collidesWithTile(float x, float y){
		
		//Reduce size for more realistic look and feel!
		tempRect.x = x+2;
		tempRect.y = y+2;
		tempRect.width = 4;
		tempRect.height = 4;
		
		//Check if rectangle collides with any tile in a 3x3 range
		for(int i = -1; i <= 1; i++){
			for(int j = -1; j <= 1; j++){
				tempTile = getLocationTile(tempRect.x+8*i, tempRect.y+8*j);
				if(tempTile!=null)
				if(tempTile.solid && tempRect.overlaps(tempTile.pos))return true;
			}
		}
		return false;
	}
	
	public Body collidesWithBody(Bullet bullet){
		
		//Reduce size for more realistic look and feel!
		tempRect.x = bullet.pos.x+1;
		tempRect.y = bullet.pos.y+1;
		tempRect.width = 6;
		tempRect.height = 6;
		
		//Check if rectangle collides with any player in a 3x3 range
		//Very efficient! only have to check collision with players located in 9 tiles
		//Instead of checking collisions across the whole map (65536 tiles!)
		//Thus even with 10000+ players and 10000+ bullets there would almost no delay
		for(int i = -1; i <= 1; i++){
			for(int j = -1; j <= 1; j++){
				tempTile = getLocationTile(tempRect.x+8*i, tempRect.y+8*j);
				if(tempTile!=null && !tempTile.solid){
					for(int k = 0; k < tempTile.players.size(); k++){
						if(tempTile.players.get(k).pos.overlaps(tempRect)){
							return tempTile.players.get(k);
						}
					}
				}
			}
		}
		return null;
	}
	
	public Tile getLocationTile(float locX, float locY){		
		int tileX = (int)((locX+Network.halfMapWidth)/8);
		int tileY = (int)((locY+Network.halfMapWidth)/8);
		
		if(tileX >= Network.MAP_W || tileX <= -1)return null;
		if(tileY >= Network.MAP_H || tileY <= -1)return null;
		
		locationTile = map[tileX][tileY];
		
		//Can't move outside of starting area if game is not started (1 minute waiting period)
		if(!gameLaunched && locationTile.biomeNum!=14)return null;
		
		return locationTile;
	}
	
	//Returns the INDEX of map position
	//For example getLocation(22.34, 8.7) -> Vector2(2,1); where (2,1) are indexes in map: map[2][1]
	public Index getLocationIndex(float locX, float locY){
		
		if(locX <= -1*Network.halfMapWidth || locX >= Network.halfMapWidth)return null;
		if(locY <= -1*Network.halfMapHeight || locY >= Network.halfMapHeight)return null;
		
		int tileX = (int)((locX+Network.halfMapWidth)/8);
		int tileY = (int)((locY+Network.halfMapWidth)/8);
		
		locationTile = map[tileX][tileY];
		
		if(locationTile.solid)return null;
		else return locationTile.index;
	}
	
	
	private void changeBodyTile(Body body){
		
		tempIndex = getLocationIndex(body.pos.x,body.pos.y);
		if(tempIndex==null)return;
		
		prevI = (int)body.tileIndex.x;
		prevJ = (int)body.tileIndex.y;
		
		nextI = (int)tempIndex.x;
		nextJ = (int)tempIndex.y;
		
		map[prevI][prevJ].players.remove((Player)body);
		map[nextI][nextJ].players.add((Player)body);
		
		body.tileIndex = tempIndex;
	}
	
	public void changeBulletTile(Bullet pr){
		
		tempIndex = getLocationIndex(pr.pos.x,pr.pos.y);
		if(tempIndex==null)return;
		
		prevI = (int)pr.tileIndex.x;
		prevJ = (int)pr.tileIndex.y;
		map[prevI][prevJ].bullets.remove(pr);
		
		nextI = (int)tempIndex.x;
		nextJ = (int)tempIndex.y;
		map[nextI][nextJ].bullets.add(pr);
		
		pr.tileIndex = tempIndex;
	}
	
	private void removeBullet(Bullet pr, int i){
		if(i < bullets.size())
		bullets.remove(i);
		prevI = (int)tempBullet.tileIndex.x;
		prevJ = (int)tempBullet.tileIndex.y;
		map[prevI][prevJ].bullets.remove(tempBullet);
	}
	
	public void gainExp(short bodyIndex, short amount){

		if(bodyIndex < players.size())
		players.get(bodyIndex).exp += amount;
		
		if(!isClient){
			serverSide.simulateGainExp(bodyIndex, amount);
		}
	}
	
	public void removeBody(short bodyIndex){
		
		//Remove the player from the main list of players
		players.remove(bodyIndex);
		
		//Reset indexes of all remaining players
		for (int i = 0; i < players.size(); i++) {
			players.get(i).index = (short) i;
		}
		
		if(!isClient){
			serverSide.simulateRemoveBody(bodyIndex);
		}
	}

	public void launchGame(short alphaId) {
		
		//TODO if(isClient){show cool particle effect - turning into alpha}
		
		//Setting alpha vampire attributes
		players.get(alphaId).statsBase[0] += 10;
		players.get(alphaId).statsBase[1] += 10;
		players.get(alphaId).statsBase[2] += 10;
		players.get(alphaId).statsBase[3] += 10;
		players.get(alphaId).statsBase[4] += 10;
		players.get(alphaId).statsBase[5] += 10;
		players.get(alphaId).statsBase[6] += 100;
		players.get(alphaId).statsBase[7] += 100;
		players.get(alphaId).recalculateStats();
		
		players.get(alphaId).vampire = true;
		players.get(alphaId).alpha = true;
		
		enableMap();
	}
	
	public void enableMap(){
		System.out.println("Enabling map!");
		
		//Setting launched state
		gameLaunched = true;
		
		//Removing starting area (all tiles in the 14th biome)
		FastNoise[] noise = new FastNoise[15];
        for(int i = 0; i < noise.length; i++){
            noise[i]=new FastNoise(new Random(Network.seed+i+1));
            noise[i].randomize();
        }
		for(int i = 0; i < biomes.get(14).tiles.size(); i++){
			float x = biomes.get(14).tiles.get(i).pos.x;
			float y = biomes.get(14).tiles.get(i).pos.y;
			biomes.get(14).tiles.get(i).biomeNum = Tile.getBiomeNum(x, y, noise, false);
		}
		
		if(isClient){
			TextureRegion[][] tilesBiome = TextureRegion.split(mainGame.spriteSheetBiomes, 8, 8);
			for(int i = 0; i < biomes.get(14).tiles.size(); i++){
				//Also replace the texture if this is client
				biomes.get(14).tiles.get(i).texture = tilesBiome[biomes.get(14).tiles.get(i).biomeNum][mainGame.rand.nextInt(4)];
			}
		}
	}
}
